<?php

namespace pmvc;

use pmvc\log\Log;
use pmvc\util\StringUtil;
use pmvc\ClassLoader;

use Exception;
use PharData;

/**
 * Implementation of the {@link ClassLoader}
 * interface that uses archive files
 */
class ArchiveClassLoader
	extends AbstractClassLoader {

	/**
	 * @var string
	 */
	private $expansionDirectory = null;

	/**
	 * @var bool
	 */
	private $expandArchives = null;

	/**
	 * @var array
	 */
	private $classpath = Array();

	/**
	 * Creates the {@link ArchiveClassLoader} and configures
	 * it for loading classes from the given archives.
	 * @param array $archives the archive files that this classloader will use
	 */
	public function __construct(
		$archives=Array(), $expandArchives=false, $expansionDirectory=null) {
		$this->expandArchives 		= $expandArchives;
		$this->expansionDirectory 	= $expansionDirectory;
		if (!is_array($archives)) {
			$archives = Array($archives);
		}
		foreach ($archives as $cp) {
			$this->addToClassPath($cp);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	protected function getClasspath() {
		return $this->classpath;
	}

	/**
	 * Adds an archive to the class path.
	 * @param string $archive the archive file
	 * @param array $roots optional roots within the archive
	 */
	public function addToClassPath($archive, $roots=Array()) {

		// make sure it's a file
		if (!file_exists($archive)) {
			throw new Exception("archive doesn't exist: $archive");
		} else if (!is_file($archive)) {
			throw new Exception("Path is not a file: $archive");
		}

		// make sure we have an array
		if (!is_array($roots)) {
			$roots = Array($roots);
		}

		// add roots from a roots folder
		$rootsFile = $archive.'.roots';
		$lines = @file_get_contents($rootsFile, false);
		if ($lines!==false) {
			foreach (preg_split('/[\r\n]/', $lines) as $line) {
				$line = preg_replace('/[\r\n]/', "", $line);
				if ($line=='/' || $line=='\\') { $line = ""; }
				$roots []= preg_replace('/[\r\n]/', '', $line);
			}
		}

		// figure out where we are expanding to
		$extractionDirectory = isset($this->expansionDirectory)
			? $this->expansionDirectory.DIRECTORY_SEPARATOR.basename($archive)."-expanded"
			: $archive."-expanded";
		$extractionTsFile = $extractionDirectory.".ts";

		// this is the root for the classpath
		$classpathRoot = null;

		// are we expanding archives?
		if ($this->expandArchives) {
			$dirExists		= file_exists($extractionDirectory);
			$tsFileExists	= file_exists($extractionTsFile);
			$isDir			= is_dir($extractionDirectory);

			// do we need to extract it?
			if (!$dirExists || !$tsFileExists) {
	
				// attempt to create the expansion directory
				if (!$dirExists && !mkdir($extractionDirectory, 0755, true)) {
					Log::warn(__CLASS__,
						"Unable to create directory {0}, can't extract archive {1}. "
						."Adding to classpath as phar://, this may perform poorly",
						$extractionDirectory, $archive);
					$classpathRoot = "phar://".$archive;
				} else {
					Log::debug(__CLASS__, "Expanding archive {0} to {1}", $archive, $extractionDirectory);
					$this->extractArchve($archive, $extractionDirectory, $extractionTsFile);
					$classpathRoot = $extractionDirectory;
				}
	
			// check to see if we need to re-expand
			} else if ($isDir) {
				$archiveMtime 	= filemtime($archive);
				$tsMtime 		= filemtime($extractionTsFile);
				$rootsMtime		= filemtime($rootsFile);
				if ($archiveMtime>$tsMtime || $rootsMtime>$tsMtime) {
					Log::debug(__CLASS__, "Re-Expanding archive {0} to {1}", $archive, $extractionDirectory);
					$this->extractArchve($archive, $extractionDirectory, $extractionTsFile);
				}
				$classpathRoot = $extractionDirectory;
	
			// file exists and is not a directory
			} else if ($dirExists && !$isDir) {
				Log::warn(__CLASS__,
					"{0} exists and is not a directory, can't extract archive {1}. "
					."Adding to classpath as phar://, this may perform poorly",
					$extractionDirectory, $archive);
				$classpathRoot = "phar://".$archive;
			}
	
		// we're not expanding archives
		} else {
			Log::debug(__CLASS__, "expandArchives is disabled for {0}, this may perform poorly", $archive);
			$classpathRoot = "phar://".$archive;
		}

		// use real path
		if ($this->shouldUseRealpath()
			&& !StringUtil::startsWith($classpathRoot, PharClassLoader::PHAR_PREFIX, true)) {
			$classpathRoot = realpath($classpathRoot);
		}

		// add the root folder if there isn't one
		if (!isset($roots) || empty($roots)) {
			$roots = Array("");
		}

		// add it to the classpath
		foreach ($roots as $root) {
			array_push($this->classpath, $classpathRoot.$root);
		}
	}

	/**
	 * Extracts an archive.
	 * @param string $archive
	 * @param string $extractionDirectory
	 * @param string $timestampFile
	 */
	private function extractArchve($archive, $extractionDirectory, $timestampFile) {
		$data = new PharData($archive);
		$data->extractTo($extractionDirectory, null, true);
		touch($timestampFile);
	}
}

?>